// SAGE - [S]illy [A]ss [G]ame [E]ngine

var util = {};
var InputHandlers = {};
var GlobalInputHandlers = {};
var SpriteSheets = {};
var AdvancedSprites = {};
var Painters = {};

var Size = Base.extend({
    initialize: function initialize(w, h) {
        this.w = w;
        this.h = h;
    }
});

var Point2D = Base.extend({
    initialize: function initialize(x, y) {
        this.x = x;
        this.y = y;
    }
});

var Point3D = Point2D.extend({
    initialize: function initialize(x, y, z) {
        Point2D.initialize.call(this, x, y);
        this.z = z;
    }
});

var Poly = Base.extend({
    initialize: function initialize(points, color) {
        this.points = points == null ? [] : points;
        this.color = color;
    }
});

var Rectangle = Base.extend({
    initialize: function initialize(x, y, w, h) {
        this.x = this.y = this.w = this.h = 0;

        if (x != null) {
            if (arguments.length == 4) {
                this.x = arguments[0];
                this.y = arguments[1];
                this.w = arguments[2];
                this.h = arguments[3];
            } else if (arguments.length == 2) {
                var p = arguments[0];
                var s = arguments[1];
                if (s.w != null && p.x != null) {
                    this.x = p.x;
                    this.y = p.y;
                    this.w = s.w;
                    this.h = s.h;
                }
            }
        }
    },
    getSize: function getSize() {
        return Size.newInstance(this.w, this.h);
    },
    setSize: function setSize(s) {
        this.w = s.w;
        this.h = s.h;
    },
    getLocation: function getLocation() {
        return Point3D.newInstance(this.x, this.y);
    },
    setLocation: function setLocation(l) {
        this.x = l.x;
        this.y = l.y;
    },
    getBottom: function getBottom() {
        return this.y - this.h;
    },
    getRight: function getRight() {
        return this.w + this.x;
    },	
    contains: function contains() {
        if (arguments.length == 1) {
            var o = arguments[0];
            if (Point2D.isPrototypeOf(o)) {
                if ((o.x < this.x) || (o.x > this.getRight()) ||
                    (o.y > this.y) || (o.y < this.getBottom())) {
                    return false;
                }
                return true;
            } else if (Rectangle.isPrototypeOf(o)) {
                if ((o.x >= this.x) && (o.getRight() <= this.getRight()) &&
                    (o.y <= this.y) && (o.getBottom() >= this.getBottom())) {
                    return true;
                }
            }
        }
        return false;
    },	
    intersects: function intersects() {
        if (arguments.length == 1) {
            var o = arguments[0];
            if (Rectangle.isPrototypeOf(o)) {
                if ((o.getRight() >= this.x) && (o.x <= this.getRight()) &&
                    (o.getBottom() <= this.y) && (o.y >= this.getBottom()) ) {
                    return true;
                }
            }
        }
        return false;
    }
});

var Coordinates2D = Base.extend({
    initialize: function initialize(x, y) {
        this.x = (x == null) ? 0 : x;
        this.y = (y == null) ? 0 : y;
        this.angleFromCamera = 0;
    },
    getPoint: function getPoint() {
        return Point2D.newInstance(this.x, this.y);
    },
    getAngleFromCameraDegrees: function getAngleFromCameraDegrees() {
        return util.degrees(this.angleFromCamera);
    },
    copyInto: function copyInto(c) {
        c.x = this.x;
        c.y = this.y;
        c.angleFromCamera = this.angleFromCamera;
        c.update();
    },
    update: function update() { }
});

var Coordinates3D = Coordinates2D.extend({
    initialize: function initialize(x, y, z) {
        Coordinates2D.initialize.call(this, x, y);
        this.z = (z == null) ? 0 : z;
        this.angleFromBearing = 0;
    },
    getPoint3D: function getPoint3D() {
        return Point3D.newInstance(this.x, this.y, this.z);
    },
    copyInto: function copyInto(c) {
        c.x = this.x;
        c.y = this.y;
        c.z = this.z;
        c.angleFromCamera = this.angleFromCamera;
        c.angleFromBearing = this.angleFromCamera;
        c.update();
    }
});

var GeoCoordinates = Coordinates3D.extend({
    initialize: function initialize(latitude, longitude) {
        this.locationRelativeToCamera = ObjectViewLocation.newInstance();
        this.latitude = latitude;
        this.longitude = longitude;
        var p = util.toECEF(latitude, longitude);
        this.x = p.x;
        this.y = p.y;
        this.angleFromCamera = 0;
        this.angleFromBearing = 0;
        //this.z = p.z; //Not sure about this from just lat and lon
        this.z = 0;
    },
    updateLocation: function updateLocation(context) {
        this.locationRelativeToCamera = util.calculateViewLocation(this.locationRelativeToCamera, context.camera, context.viewPort, this, true, false, true);
    }
});

var Placement = Coordinates3D.extend({
    initialize: function initialize() {
        Coordinates3D.initialize.call(this);
        this.h = this.w = this.bearing = this.totalMovement = this.lineOfSight = 0;
        this.clickable = false;
        this.hasBearing = true;
        this.lineOfSightIndependent = false;
    },
    setBearing: function setBearing(angle) 
    {
        if (this.lineOfSightIndependent) {
            this.bearing = angle;
        }
        else {
            if (this.lineOfSight == this.bearing)
                this.lineOfSight = this.bearing = angle;
            else {
                //var diff = radians - this.bearing;
                this.bearing = angle;
                //this.lineOfSight = radians + diff;
                //if (this.bearing > this.lineOfSight) this.lineOfSight = radians - diff;
                //else this.lineOfSight = radians + diff;
                //while (this.lineOfSight < 0) this.lineOfSight = this.lineOfSight + util._360;
            }
        }
    },
    getBearingDegrees: function getBearingDegrees() {
        return util.degrees(this.bearing);
    },
    getLineOfSightDegrees: function getLineOfSightDegrees() {
        return util.degrees(this.lineOfSight);
    },
    normalizeBearingToAngle: function normalizeBearingToAngle(moveToAngle, incrementAngle) {
        if (incrementAngle == null) incrementAngle = util.degrees(10);
        var a = util.moveTowardsAngle(this.bearing, moveToAngle, incrementAngle);
        if (a == null) this.setBearing(moveToAngle);
        else this.turn(a);
    },
    normalizeLineOfSightToAngle: function normalizeLineOfSightToAngle(moveToAngle, incrementAngle) {
        if (incrementAngle == null) incrementAngle = util.degrees(10);
        var a = util.moveTowardsAngle(this.lineOfSight, moveToAngle, incrementAngle);
        if (a == null) this.lineOfSight = moveToAngle;
        else this.turnLineOfSight(a);
    },
    normalizeLineOfSightToBearing: function normalizeLineOfSightToBearing(incrementAngle) {
        this.normalizeLineOfSightToAngle(this.bearing, incrementAngle);
    },
    getRectangle: function getRectangle() {
        return Rectangle.newInstance(this.x, this.y, this.w, this.h);
    },
    move: function move(distance) {
        this.x += distance * Math.cos(this.bearing);
        this.y += distance * Math.sin(this.bearing);
        this.totalMovement += distance;
    },
    sidestep: function sidestep(distance) {
        var degrees = this.bearing + ((distance < 0) ? util._270 : (util._90 * -1));
        this.x += distance * Math.cos(degrees);
        this.y += distance * Math.sin(degrees);
    },
    turn: function turn(angleOfChange) {
        if (isNaN(angleOfChange)) alert("NaN warning: turn");
        var b = this.bearing + angleOfChange;
        while (b < 0) b = b + util._360;
        if (b >= util._360) b = (b % util._360);
        this.setBearing(b);
        //this.lineOfSight += radians;
        //while (this.lineOfSight < 0) this.lineOfSight = this.lineOfSight + util._360;
        //if (this.lineOfSight >= util._360) this.lineOfSight = (this.lineOfSight % util._360);
    },
    turnLineOfSight: function turnLineOfSight(angleOfChange) {
        if (isNaN(angleOfChange)) alert("NaN warning: turnLineOfSight");
        var l = this.lineOfSight + angleOfChange;
        while (l < 0) l = l + util._360;
        if (l >= util._360) l = (l % util._360);
        this.lineOfSight = l;
    },
    copyInto: function copyInto(c) {
        c.x = this.x;
        c.y = this.y;
        c.z = this.z;
        c.h = this.h;
        c.w = this.w;
        c.bearing = this.bearing;
        c.lineOfSight = this.lineOfSight;
        c.totalMovement = this.totalMovement;
        c.angleFromCamera = this.angleFromCamera;
        c.angleFromBearing = this.angleFromBearing;
        c.update();
    }
});

var PointViewLocation = Base.extend({
    initialize: function initialize() {
	    this.placement = Placement.newInstance();
	    this.distance = 0.0;
	    this.distance3D = 0.0;
	    this.visible = true;
	    this.point = Point2D.newInstance(0, 0);
	    this.shadowPoint = Point2D.newInstance(0, 0);
    }
});

var ObjectViewLocation = PointViewLocation.extend({
    initialize: function initialize() {
        PointViewLocation.initialize.call(this);
        this.objectRectangle = Rectangle.newInstance();
        this.collisionRectangle = Rectangle.newInstance();
        this.shadowRectangle = Rectangle.newInstance();
    }
});

var PolyObject = Base.extend({
    initialize: function initialize() {
        this.locationRelativeToCamera = ObjectViewLocation.newInstance();
        this.distance = 0.0;
        this.distance3D = 0.0;
        this.visible = true;
        this.objectId = 0;
        this.speed = 0.0;
        this.frameIndex = 0;
        this.frameIncrement = 1;
        this.color = null;
        this.objectName = null;
        this.painter = null;
        this.sprite = null;
        this.path = [];
        this.polygons = [];
        this.relativePolygons = [];
    },
    updateLocation: function updateLocation(context) {
        this.relativePolygons = [];
        for (var i = 0; i < this.polygons; i++) {
            var poly = this.polygons[i];
            this.visible = true;

            var relativePoly = Poly.newInstance();

            for (var j = 0; j < poly.points; j++) {
                relativePoly.points[j] = util.findRelativeLocation(context.camera, poly.points[i]/*, relativeLocation*/);
            }

            if (util.clipPolygonOnYAxis(relativePoly, context.viewPort.DistanceToView)) {
                this.relativePolygons.push(relativePoly);
            }
        }
        if (this.relativePolygons.length == 0) this.visible = false;
    },
    tick: function tick() { },
    copyInto: function copyInto(c) {
        c.x = this.x;
        c.y = this.y;
        c.z = this.z;
        c.h = this.h;
        c.w = this.w;
        c.bearing = this.bearing;
        c.lineOfSight = this.lineOfSight;
        c.totalMovement = this.totalMovement;
        c.angleFromCamera = this.angleFromCamera;
        c.angleFromBearing = this.angleFromBearing;
        c.objectId = this.objectId;
        c.speed = this.speed;
        c.frameIndex = this.frameIndex;
        c.frameIncrement = this.frameIncrement;
        c.color = this.color;
        c.objectName = this.objectName;
        c.painter = this.painter;
        c.sprite = this.sprite;
        c.path = this.path;
        c.update();
    }
});

var GameObject = Placement.extend({
    initialize: function initialize() {
        Placement.initialize.call(this);
        this.locationRelativeToCamera = ObjectViewLocation.newInstance();
        this.objectId = 0;
        this.speed = 0.0;
        this.frameIndex = 0;
        this.frameIncrement = 1;
        this.color = null;
        this.objectName = null;
        this.painter = null;
        this.sprite = null;
        this.path = [];
    },
    updateLocation: function updateLocation(context) {
        this.locationRelativeToCamera = util.calculateViewLocation(this.locationRelativeToCamera, context.camera, context.viewPort, this);
    },
    tick: function tick() { },
    copyInto: function copyInto(c) {
        c.x = this.x;
        c.y = this.y;
        c.z = this.z;
        c.h = this.h;
        c.w = this.w;
        c.bearing = this.bearing;
        c.lineOfSight = this.lineOfSight;
        c.totalMovement = this.totalMovement;
        c.angleFromCamera = this.angleFromCamera;
        c.angleFromBearing = this.angleFromBearing;
        c.objectId = this.objectId;
        c.speed = this.speed;
        c.frameIndex = this.frameIndex;
        c.frameIncrement = this.frameIncrement;
        c.color = this.color;
        c.objectName = this.objectName;
        c.painter = this.painter;
        c.sprite = this.sprite;
        c.path = this.path;
        c.update();
    }
});

var ViewPort = Base.extend({
    initialize: function initialize(unitWidth, horizontalViewableAngle, verticalViewableAngle) {
        this.unitHeight = 3;
        this.unitWidth = (unitWidth == null ? 6 : unitWidth);
        this.scale = null;
        this.size = Size.newInstance(200, 100);
        this.distanceToView = null;
        this.horizontalViewableAngle = horizontalViewableAngle;
        this.verticalViewableAngle = verticalViewableAngle;

        this.recalculate();
    },
    getWidthOfView: function getWidthOfView(width) {
        this.unitWidth = width;
        this.recalculate();
    },	
	setHeightOfView: function setHeightOfView(height) {
        this.unitHeight = height;
        this.recalculate(true);
    },	
    setHorizontalViewableAngle: function setHorizontalViewableAngle(angle) {
        this.horizontalViewableAngle = angle;
        this.recalculate();
    },	
    setVerticalViewableAngle: function setVerticalViewableAngle(angle) {
        this.verticalViewableAngle = angle;
        this.recalculate(true);
    },	
    isInView: function isInView(angle) {
        return true;
    },	
    getScale: function getScale() {
        if (this.unitWidth >= 0) {
            return this.size.w / this.unitWidth;
        }
        else {
            return 1;
        }
    },	
    setPixelSize: function setPixelSize() {
        if (arguments.length == 1){
            this.size.w = arguments[0].w;
            this.size.h = arguments[0].h;
        }
        else if (arguments.length == 2) {
            this.size.w = arguments[0];
            this.size.h = arguments[1];
        }
        this.recalculate();
    },	
    recalculate: function recalculate(useHeight) {
        if (useHeight == null)
            useHeight = false;
		
        if (useHeight && this.unitHeight > 0) {
            this.scale = this.size.h / this.unitHeight;
            this.unitWidth = this.size.w / (this.scale == 0 ? 1 : this.scale);
            this.distanceToView = (this.unitHeight / 2.0) / Math.atan(this.verticalViewableAngle);
        } else if (this.unitWidth > 0) {
            this.scale = this.size.w / this.unitWidth;
            this.unitHeight = this.size.h / (this.scale == 0 ? 1 : this.scale);
            this.distanceToView = (this.unitWidth / 2.0) / Math.atan(this.horizontalViewableAngle);
        }
    }	
});

var SpriteSheet = Base.extend({
    initialize: function initialize(imgUrl, columns, rows) {
        var me = this;
        this.sheetSize = this.image = this.w = this.h = null;
        this.columns = columns;
        this.rows = rows;
        this.directionRowArray = ['front', 'left', 'right', 'back'];
        this.directions = 4;

        var imageObj = new Image();
        imageObj.src = imgUrl;
        imageObj.onload = function () {
            me.sheetSize = Size.newInstance(this.width, this.height);
            me.image = this;
            me.w = me.sheetSize.w / me.columns;
            me.h = me.sheetSize.h / me.rows;
        };
    },
    isReady: function isReady() {
        return (this.image != null);
    },
    /*rotate: function rotate(d) {
        if (isNaN(d))
            this.setDirection(d);
        else {
            var r = ['f', 'r', 'b', 'l'];
            var i = r.indexOf(this.direction) + x;
            if (i < 0)
                i = 3;
            i = i % 4;
            this.setDirection(r[i]);
        }
    },*/
    getRowIndex: function getRowIndex(dir) {
        var r = this.directionRowArray;
        //return (state == "fly") ? this.directionRowArray.indexOf(dir) : this.flyDirectionRowArray.indexOf(dir);
        return r.indexOf(dir)
    },
    getFrameCount: function getFrameCount(state) {
        if (this.image == null) return 0;
        return (state.indexOf('still') > -1) ? 1 : this.columns;
    },
    getXOff: function getXOff(col) {
        if (this.image == null) return 0;
        return this.w * col;
    },
    getYOff: function getYOff(dir) {
        if (this.image == null) return 0;
        return this.h * this.getRowIndex(dir);
    }
});

var AdvancedSprite = Base.extend({
    initialize: function initialize(imgUrl, name, ext, frameCounts, bounce) {
        var me = this;
        if (frameCounts == null) {
            this.frameCounts = {
                "walk": { "front": 6, "left": 8, "right": 8, "back": 6 },
                "run": { "front": 6, "left": 8, "right": 8, "back": 6 },
                "still": { "front": 1, "left": 1, "right": 1, "back": 1 }
            };
        }
        else this.frameCounts = frameCounts;

        this.bounce = bounce == null ? false : bounce;
        this.frameFiles = {};
        this.imageCount = 0;
        this.directions = 0;
        this.imagesLoaded = 0;
        this.name = name;
        this.ext = ext == null ? ".gif" : ext;
        this.baseUrl = imgUrl;

        var stIdx = 0;
        for (state in this.frameCounts) {
            var dirs = this.frameCounts[state];
            
            for (dir in dirs) {
                if (stIdx == 0) this.directions++;
                var count = dirs[dir];
                for (var i = 1; i <= count; i++) {
                    var furlKey = state + "-" + dir + "-" + i;
                    var furl = imgUrl + name + "-" + furlKey + this.ext;
                    var imageObj = new Image();
                    imageObj.src = furl;
                    imageObj.onload = function () {
                        me.imagesLoaded++;
                    };

                    this.frameFiles[furlKey] = imageObj;
                    this.imageCount++;
                }
            }
            stIdx++;
        }
    },
    isReady: function isReady() {
        return (this.imageCount > 0 && this.imageCount <= this.imagesLoaded);
    },
    supportsState: function supportsState(state) {
        return (this.frameCounts[state] != null);
    },
    getFrameCount: function getFrameCount(state, direction) {
        if (!this.isReady()) return 0;
        return this.frameCounts[state][direction];
    },
    getImage: function getImage(state, direction, frameIndex) {
        if (!this.isReady()) return 0;
        return this.frameFiles[state + "-" + direction + "-" + (frameIndex + 1)];
    },
    getXOff: function getXOff(state, direction, frameIndex) {
        if (!this.isReady()) return 0;
        return 0;
    },
    getYOff: function getYOff(state, direction, frameIndex) {
        if (!this.isReady()) return 0;
        return 0;
    },
    getWidth: function getWidth(state, direction, frameIndex) {
        if (!this.isReady()) return 0;
        return this.getImage(state, direction, frameIndex).width;
    },
    getHeight: function getHeight(state, direction, frameIndex) {
        if (!this.isReady()) return 0;
        return this.getImage(state, direction, frameIndex).height;
    }
});

var PaintingContext = Base.extend({
    initialize: function initialize() {
        this.camera = null;
        this.frontLayer = null;
        this.backLayer = null;
        this.gameObject = null;
        this.viewPort = null;
    }
});

function Utility() {

	this.DEG2RAD = Math.PI / 180.0;
	this.RAD2DEG = 180.0 / Math.PI;
	this.origin2D = Coordinates2D.newInstance();
	this.origin3D = Coordinates3D.newInstance();
	this.placementRule = "2D-Flat"; // 2D, 3D, and 2D-Flat (default)
	
	this.random = function (a, b) {
	    if (a == null) return Math.random();
	    else if (b == null) return Math.round(Math.random() * a);
	    else return Math.round((Math.random() * (b-a)) + a);
	};

	this.radians = function (degrees) {
		return degrees * this.DEG2RAD;
	};
	
	this.degrees = function (radians) {
		return radians * this.RAD2DEG;
	};
	
	this._45 = this.radians(45);
	this._90 = this.radians(90);
	this._135 = this.radians(135);
	this._180 = this.radians(180);
	this._225 = this.radians(225);
	this._270 = this.radians(270);
	this._315 = this.radians(315);
	this._360 = this.radians(360);

	//this.toECEF = function (lat, lon) {
	//    var cosLat = Math.cos(this.radians(lat)),
	//        sinLat = Math.sin(this.radians(lat)),
	//        cosLon = Math.cos(this.radians(lon)),
	//        sinLon = Math.sin(this.radians(lon)),
    //        rad = 6378137.0,
	//        f = 1.0 / 298.257224,
    //        h = 0.0,
    //        C = 1.0 / Math.sqrt(cosLat * cosLat + (1.0 - f) * (1.0 - f) * sinLat * sinLat),
    //        S = (1.0 - f) * (1.0 - f) * C,
    //        coords = [];
	//    coords[0] = (rad * C + h) * cosLat * cosLon; // x
	//    coords[1] = (rad * C + h) * cosLat * sinLon; // y
	//    coords[2] = (rad * S + h) * sinLat; // z
	//    return coords;
	//};

	this.toECEF = function (lat, lon) {
	    var cosLat = Math.cos(this.radians(lat)),
	        sinLat = Math.sin(this.radians(lat)),
	        cosLon = Math.cos(this.radians(lon)),
	        sinLon = Math.sin(this.radians(lon)),
            rad = 6378137.0,
	        f = 1.0 / 298.257224,
            h = 0.0,
            C = 1.0 / Math.sqrt(cosLat * cosLat + (1.0 - f) * (1.0 - f) * sinLat * sinLat),
            S = (1.0 - f) * (1.0 - f) * C,
            coords = Point3D.newInstance();

	    coords.x = (rad * C + h) * cosLat * cosLon; // x
	    coords.y = (rad * C + h) * cosLat * sinLon; // y
	    coords.z = (rad * S + h) * sinLat; // z
	    return coords;
	};

    //this.toECEFJ = function (lat, lon) {
	//    var rad = 6378137.0, // radius of the earth in meters
    //        prll = 111320.0, // meters for one latitudinal parallel
    //        qtrrad = ((Math.PI * rad) / 2.0),
    //        ratioTo90 = (90.0-Math.abs(lat)) / 90.0,
    //        coords = Point3D.newInstance();

	//    coords.x = lon * (prll * ratioTo90) + rad*2.0; // x
	//    coords.y = lat * prll + rad; // y
	//    coords.z = 0; // y
	//    return coords;
	//};

	this.drawEllipse = function (ctx, x, y, w, h) {
	    var kappa = .5522848;
	    ox = (w / 2) * kappa, // control point offset horizontal
        oy = (h / 2) * kappa, // control point offset vertical
        xe = x + w,           // x-end
        ye = y + h,           // y-end
        xm = x + w / 2,       // x-middle
        ym = y + h / 2;       // y-middle

	    ctx.beginPath();
	    ctx.moveTo(x, ym);
	    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
	    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
	    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
	    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
	    ctx.closePath();
	    ctx.stroke();
	};

	this.fillEllipse = function (ctx, x, y, w, h) {
	    var kappa = .5522848;
	    ox = (w / 2) * kappa, // control point offset horizontal
        oy = (h / 2) * kappa, // control point offset vertical
        xe = x + w,           // x-end
        ye = y + h,           // y-end
        xm = x + w / 2,       // x-middle
        ym = y + h / 2;       // y-middle

	    ctx.beginPath();
	    ctx.moveTo(x, ym);
	    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
	    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
	    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
	    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
	    ctx.closePath();
	    ctx.fill();
	};

	this.determine4WayDirection = function (angle) {
	    var a = angle > util._360 ? (angle % this._360) : angle;

	    if ((a >= 0 && a < Math.PI / 4.0) || (a >= this._315 && a <= (Math.PI * 2.0)))
	        return 'back';
	    else if (a >= this._45 && a < this._135)
	        return 'left';
	    else if (a >= this._135 && a < this._225)
	        return 'front';
	    else
	        return 'right';
	};

	this.determine8WayDirection = function (angle) {
	    var a = angle > util._360 ? (angle % this._360) : angle;

	    if ((a >= 0 && a < this.radians(22.5)) || (a >= this.radians(337.5) && a <= this.radians(360)))
	        return 'back';
	    else if (a >= this.radians(22.5) && a < this.radians(67.5))
	        return 'backleft';
	    else if (a >= this.radians(67.5) && a < this.radians(112.5))
	        return 'left';
	    else if (a >= this.radians(112.5) && a < this.radians(157.5))
	        return 'frontleft';
	    else if (a >= this.radians(157.5) && a < this.radians(202.5))
	        return 'front';
	    else if (a >= this.radians(202.5) && a < this.radians(247.5))
	        return 'frontright';
	    else if (a >= this.radians(247.5) && a < this.radians(292.5))
	        return 'right';
	    else
	        return 'backright';
	};
	
	this.orderByDistance = function (gameObjects) {
	    gameObjects.sort(function(a, b) {
	        var da = a.locationRelativeToCamera.distance;
	        var db = b.locationRelativeToCamera.distance;
	        return ((da > db) ? -1 : ((da < db) ? 1 : 0));
	    });
	};

    //double AngleToPoint(this Coordinates3D first, Coordinates3D toPoint)
	this.angleToPoint = function(first, toPoint) {
		return Math.atan2(toPoint.y - first.y, toPoint.x - first.x);
	};
	
	//double FindDistance2D(ICoordinates2D a, ICoordinates2D b)
	this.findDistance2D = function(a, b) {
		return Math.sqrt(Math.pow((a.x - b.x), 2) + Math.pow((a.y - b.y), 2));
	};
	
	//double FindDistance3D(ICoordinates3D a, ICoordinates3D b)
	this.findDistance3D = function(a, b) {
		var longLeg = this.findDistance2D(a, b),
			shortLeg = Math.abs(a.z - b.z),
			hypotenuse = shortLeg === 0 ? longLeg : Math.sqrt(Math.pow(longLeg, 2) + Math.pow(shortLeg, 2));

		return hypotenuse;
	};
	
	//public static double? GetAngle(ICoordinates2D c1, ICoordinates2D c2)
	this.getAngle = function(c1, c2) {
		var angle = null;
		if (c1.x != c2.x || c1.y != c2.y)
		{
			angle = Math.atan((c1.x - c2.x) / (c1.y - c2.y));
		}
		return angle;
	};
	
	this.clipPolygonOnYAxis = function (polygon, xShift) {
	    if (xShift == null) xShift = 0.0;

	    var adjustedPoints = [];
	    var prevPoint = polygon.points[polygon.length - 1]; // prevPoint starts as the last point.
        
	    for (var i = 0; i < polygon.length; i++) {
	        var point = polygon[i],
                prevX = prevPoint.x - xShift,
                currX = point.x - xShift;
	        //do check on y...
	        if (prevX >= 0)
	        {
	            adjustedPoints.push(prevPoint);
	        }

            if ((prevX >= 0 && currX < 0) || (prevX < 0 && currX >= 0))
            {
	            // Slope intercept crap: y = mx + b => b = y - mx, m = (y2-y1) / (x2-x1)
	            var yIntercept = point.y - ((prevPoint.y - point.y) / (prevX - currX)) * currX;
	            var zIntercept = point.z - ((prevPoint.z - point.z) / (prevX - currX)) * currX;
	            var pnew = Placement.newInstance();
	            prevPoint.copyInto(pnew);
	            pnew.X = xShift;
	            pnew.Y = yIntercept;
	            pnew.Z = zIntercept;
	            adjustedPoints.Add(pnew);
            }

            prevPoint = point;
	    }

	    polygon.points = adjustedPoints;
	    return (polygon.points.length >= 3);
	};

	this.getRelativeAngle = function (relativePlacement, x1, y1, angle) {
	    var x = x1, y = y1;
	    if (angle != null) {
	        x = (Math.cos(angle) * x1) - (Math.sin(angle) * y1);
	        y = (Math.sin(angle) * x1) + (Math.cos(angle) * y1);
	    }

	    var relativeAngleToX = Math.atan2(Math.abs(y), Math.abs(x)) *
			    (y < 0 && x > 0 ? -1.0 : 1.0);
	    var relativeAngle = relativeAngleToX;

	    if (x < 0 && y < 0) relativeAngle = Math.abs(relativeAngleToX) + this._180;
	    else if (x > 0 && y < 0) relativeAngle = this._360 - Math.abs(relativeAngleToX);
	    else if (x < 0 && y > 0) relativeAngle = this._180 - Math.abs(relativeAngleToX);

	    if (relativeAngle < 0) relativeAngle = this._360 + relativeAngle;
	    relativeAngle = relativeAngle > this._360 ? (this._360 % relativeAngle) : relativeAngle;

	    return relativeAngle;
	};

	this.findRelativeLocation = function(origin, location, relativePlacement) {
	    if (relativePlacement == null) relativePlacement = Placement.newInstance();
	    location.copyInto(relativePlacement);

	    var relativeAngleToX, relativeAngle,
			translatedX = (location.x - origin.x),
			translatedY = (location.y - origin.y),
			reverseAngle = -1 * origin.lineOfSight;
        
	    relativePlacement.x = (Math.cos(reverseAngle) * translatedX) - (Math.sin(reverseAngle) * translatedY);
	    relativePlacement.y = (Math.sin(reverseAngle) * translatedX) + (Math.cos(reverseAngle) * translatedY);

	    relativePlacement.angleFromCamera = this.getRelativeAngle(relativePlacement, relativePlacement.x, relativePlacement.y);

	    if (origin.lineOfSight == origin.bearing) {
	        relativePlacement.angleFromBearing = relativePlacement.angleFromCamera;
	    }
	    else {
	        relativePlacement.angleFromBearing = this.getRelativeAngle(relativePlacement, translatedX, translatedY, (-1 * origin.bearing));
	    }

	    if (("bearing" in location) && ("hasBearing" in location && location.hasBearing)) { // if this is just a point in space, it may not have a bearing, so ignore that calculation.

	        relativePlacement.bearing = location.bearing - origin.lineOfSight;
	        if (relativePlacement.bearing < 0) relativePlacement.bearing = this._360 + relativePlacement.bearing;
	        relativePlacement.bearing = relativePlacement.bearing > this._360 ? (this._360 % relativePlacement.bearing) : relativePlacement.bearing;
	    }
		return relativePlacement;
	};

	this.moveTowardsAngle = function (normalizeFrom, normalizeTo, incrementAngle) {
	    if (normalizeFrom == null && isNaN(normalizeFrom)) alert("NaN warning: normalizeFrom Angle bearing");
	    if (normalizeTo == null && isNaN(normalizeTo)) alert("NaN warning: normalizeTo Angle");

	    var angleToTurn = null;

	    if (incrementAngle != null && Math.abs(normalizeTo - normalizeFrom) > Math.abs(incrementAngle * 0.1)) {
	        var b = normalizeTo + util._360,
                l = normalizeFrom + util._360,
                normalizeToIsBigger = (normalizeTo > normalizeFrom),
                diff1 = normalizeToIsBigger ? normalizeTo - normalizeFrom : normalizeFrom - normalizeTo,
                diff2 = normalizeToIsBigger ? l - normalizeTo : b - normalizeFrom,
                multiplier = (diff1 > diff2) ? (normalizeToIsBigger ? -1 : 1) : (normalizeToIsBigger ? 1 : -1),
                diff = (diff1 > diff2) ? diff2 : diff1;

	        if (diff < util.radians(3)) incrementAngle *= 0.05;
	        else if (diff < util.radians(5)) incrementAngle *= 0.10;
	        else if (diff < util.radians(10)) incrementAngle *= 0.20;
	        else if (diff < util.radians(30)) incrementAngle *= 0.60;
	        else if (diff < util.radians(50)) incrementAngle *= 0.90;

	        angleToTurn = Math.abs(incrementAngle) * multiplier;
	    }
	    return angleToTurn;
	};

	this.calculateViewLocation = function (viewLocation, camera, viewPort, location, isPointLocation, hasAlreadyBeenCalculated, calculateIfOffScreen) {
	    isPointLocation = isPointLocation == null ? false : isPointLocation;
	    hasAlreadyBeenCalculated = hasAlreadyBeenCalculated == null ? false : hasAlreadyBeenCalculated;
	    calculateIfOffScreen = calculateIfOffScreen == null ? false : calculateIfOffScreen;
	    viewLocation.visible = true;
		try
		{
		    var relativeLocation = null;

			if (hasAlreadyBeenCalculated && viewLocation.placement != null) {
			    relativeLocation = viewLocation.placement;
			}
			else {
			    relativeLocation = this.findRelativeLocation(camera, location);
				viewLocation.placement = relativeLocation;
			}

			var angle = (relativeLocation.angleFromCamera < this._180) ? relativeLocation.angleFromCamera : this._360 - relativeLocation.angleFromCamera;

			if (relativeLocation.x <= 0.0 ||
				(angle > (viewPort.horizontalViewableAngle / 2.0) + this.radians(2.0)))
			{
				viewLocation.visible = true;
			}

			var doCalculations = (viewLocation.visible || (calculateIfOffScreen && relativeLocation.x > 0));
			if (doCalculations)
			{
				// Pick one of the three above.
				var unitDistance = 0,
					top = relativeLocation.z + relativeLocation.h,
					bottom = relativeLocation.z,
					cameraHeight = camera.h + camera.z;

				var org = Coordinates3D.newInstance();
				org.z = cameraHeight;

				var unitDistance3D = this.findDistance3D(org, relativeLocation) * (relativeLocation.x > 0 ? 1 : -1);
				switch (this.placementRule)
				{
					case "2D":
					    unitDistance = this.findDistance2D(org, relativeLocation) * (relativeLocation.x > 0 ? 1 : -1);
						break;
					case "3D":
						unitDistance = unitDistance3D;
						break;
					default:
						unitDistance = relativeLocation.x;
						break;
				}

				if (unitDistance < viewPort.distanceToView) {
				    viewLocation.visible = false;
				    if (doCalculations) {
				        viewLocation.distance = unitDistance;
				        viewLocation.distance3D = unitDistance3D;
				    }
				}
				else {
				    viewLocation.distance = unitDistance;
				    viewLocation.distance3D = unitDistance3D;
				    var pointOffsetFromHorizon_Ground = (cameraHeight / unitDistance) * viewPort.distanceToView;
				    var pointOffsetFromHorizon_Bottom = (Math.abs(bottom - cameraHeight) / unitDistance) * viewPort.distanceToView;
				    var pointOffsetFromHorizon_Top = (Math.abs(top - cameraHeight) / unitDistance) * viewPort.distanceToView;

				    var pointOffsetFromVerticalHorizon = (relativeLocation.y / unitDistance) * viewPort.distanceToView;

				    // Shadow
				    var unitShadowHeight = Math.abs(((cameraHeight / (unitDistance + relativeLocation.w)) * viewPort.distanceToView) - pointOffsetFromHorizon_Ground);

				    pointOffsetFromHorizon_Ground = (cameraHeight - pointOffsetFromHorizon_Ground);

				    pointOffsetFromHorizon_Bottom = (bottom > cameraHeight)
						? (cameraHeight + pointOffsetFromHorizon_Bottom) : (cameraHeight - pointOffsetFromHorizon_Bottom);

				    pointOffsetFromHorizon_Top = (top > cameraHeight)
						? (cameraHeight + pointOffsetFromHorizon_Top) : (cameraHeight - pointOffsetFromHorizon_Top);

				    //var horizontalOffset = Math.round(pointOffsetFromVerticalHorizon * viewPort.scale) * (relativeLocation.bearing >= 0 ? -1.0 : 1.0);
				    var horizontalOffset = Math.round(pointOffsetFromVerticalHorizon * viewPort.scale) * -1.0;
				    if (isPointLocation) {
				        var point = Point2D.newInstance();

				        point.y = (viewPort.size.h / 2) + Math.round((cameraHeight - pointOffsetFromHorizon_Top) * viewPort.scale);
				        point.x = (viewPort.size.w / 2) + horizontalOffset;

				        viewLocation.point = point;

				        var shadowPoint = Point2D.newInstance();

				        shadowPoint.y = (viewPort.size.h / 2) + Math.round((cameraHeight - pointOffsetFromHorizon_Ground) * viewPort.scale);
				        shadowPoint.x = point.x;


				        viewLocation.shadowPoint = shadowPoint;
				    }
				    else {
				        // shadow calc
				        var shadowHeight = Math.round(unitShadowHeight * viewPort.scale);

				        var objectRectangle = Rectangle.newInstance();
				        objectRectangle.h = Math.round((pointOffsetFromHorizon_Top - pointOffsetFromHorizon_Bottom) * viewPort.scale);
				        if (location.h > 0) objectRectangle.w = Math.round((location.w * objectRectangle.h) / location.h);
				        else objectRectangle.w = 0;
				        objectRectangle.y = (viewPort.size.h / 2) + Math.round((cameraHeight - pointOffsetFromHorizon_Top) * viewPort.scale);
				        objectRectangle.x = (viewPort.size.w / 2) - (objectRectangle.w / 2) + horizontalOffset;

				        var shadowRectangle = Rectangle.newInstance();
				        shadowRectangle.h = shadowHeight;
				        shadowRectangle.w = objectRectangle.w;
				        shadowRectangle.x = objectRectangle.x;
				        shadowRectangle.y = (viewPort.size.h / 2) + Math.round((cameraHeight - pointOffsetFromHorizon_Ground) * viewPort.scale) - (shadowHeight / 2);

				        viewLocation.objectRectangle = objectRectangle;
				        viewLocation.shadowRectangle = shadowRectangle;
				    }
				}
			}
		}
		catch (err)
		{
			alert(""+err.number + " - " + err.description);
		}
		return viewLocation;
	};
	
	/*	
        public static bool ClipPolygonOnYAxis(List<IPlacement> polygon, double xShift = 0)
        {
            List<IPlacement> fixedPoints = new List<IPlacement>();
            IPlacement point,
            prevPoint = polygon[polygon.Count - 1];
            for (int i = 0; i < polygon.Count; i++)
            {
                point = polygon[i];
                double prevX = prevPoint.X - xShift;
                double currX = point.X - xShift;
                // do check on y...
                if (prevX >= 0)
                {
                    fixedPoints.Add(prevPoint);
                }

                if ((prevX >= 0 && currX < 0) || (prevX < 0 && currX >= 0))
                {
                    // Slope intercept crap: y = mx + b => b = y - mx, m = (y2-y1) / (x2-x1)
                    var yIntercept = point.Y - ((prevPoint.Y - point.Y) / (prevX - currX)) * currX;
                    var zIntercept = point.Z - ((prevPoint.Z - point.Z) / (prevX - currX)) * currX;
                    Placement pnew = new Placement();
                    prevPoint.CopyInto(pnew);
                    pnew.X = xShift;
                    pnew.Y = yIntercept;
                    pnew.Z = zIntercept;
                    fixedPoints.Add(pnew);
                }

                prevPoint = point;
            }
            polygon.Clear();
            polygon.AddRange(fixedPoints);

            return (fixedPoints.Count >= 3);
        }

        /// <summary>
        /// Expands hard points into points based on the movement distance.
        /// </summary>
        public static List<Coordinates3D> ExpandPath(this List<Coordinates3D> path, double interval)
        {
            // Temporary
            int Z = 0;
            List<Coordinates3D> expandedPath = new List<Coordinates3D>();
            for (int i = 0; i < path.Count - 1; i++)
            {
                double angle = path[i].AngleToPoint(path[i + 1]);
                expandedPath.Add(path[i]);

                double distanceBetweenHardPoints = Utility.FindDistance3D(path[i], path[i + 1]);
                for (int j = 0; j < (distanceBetweenHardPoints - interval) / interval; j++)
                {
                    double x = path[i].X + interval * (j + 1) * Math.Cos(angle);
                    double y = path[i].Y + interval * (j + 1) * Math.Sin(angle);

                    // Add new dynamic point.
                    expandedPath.Add(new Coordinates3D(x, y, Z));
                }
            }
            return expandedPath;
        }


        /// <summary>
        /// Creates the rounded rectangle path.
        /// </summary>
        /// <param name="rect">The rect.</param>
        /// <param name="cornerRadius">The corner radius.</param>
        /// <returns></returns>
        public static GraphicsPath CreateRoundedRectanglePath(Rectangle rect, int cornerRadius)
        {
            GraphicsPath roundedRect = new GraphicsPath();
            roundedRect.AddArc(rect.X, rect.Y, cornerRadius * 2, cornerRadius * 2, 180, 90);
            roundedRect.AddLine(rect.X + cornerRadius, rect.Y, rect.Right - cornerRadius * 2, rect.Y);
            roundedRect.AddArc(rect.X + rect.Width - cornerRadius * 2, rect.Y, cornerRadius * 2, cornerRadius * 2, 270, 90);
            roundedRect.AddLine(rect.Right, rect.Y + cornerRadius * 2, rect.Right, rect.Y + rect.Height - cornerRadius * 2);

            roundedRect.AddArc(rect.X + rect.Width - cornerRadius * 2, rect.Y + rect.Height - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90);

            roundedRect.AddLine(rect.Right - cornerRadius * 2, rect.Bottom, rect.X + cornerRadius * 2, rect.Bottom);
            roundedRect.AddArc(rect.X, rect.Bottom - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 90, 90);
            roundedRect.AddLine(rect.X, rect.Bottom - cornerRadius * 2, rect.X, rect.Y + cornerRadius * 2);
            roundedRect.CloseFigure();
            return roundedRect;
        }

        public static Bitmap CreateMask(Bitmap image, Color transparencyColor)
        {
            Bitmap mask = new Bitmap(image.Width, image.Height);
            for (int y = 0; y < image.Height; y++)
            {
                for (int x = 0; x < image.Width; x++)
                {
                    Color pixel = image.GetPixel(x, y);
                    if (pixel.A == transparencyColor.A &&
                        pixel.R == transparencyColor.R &&
                        pixel.G == transparencyColor.G &&
                        pixel.B == transparencyColor.B)
                    {
                        mask.SetPixel(x, y, Color.White);
                    }
                    else
                    {
                        mask.SetPixel(x, y, Color.Black);
                    }
                }
            }
            mask.Save(@"masktest.png", ImageFormat.Png);
            image.Save(@"realimage.png", ImageFormat.Png);
            return mask;
        }

        /// <summary>
        /// Adjusts the brightness matrix.
        /// </summary>
        /// <param name="img">The img.</param>
        /// <param name="value">The value.</param>
        public static void AdjustBrightnessMatrix(Bitmap img, int value)
        {
            if (value == 0) // No change, so just return
                return;

            float sb = (float)value / 255F;
            float[][] colorMatrixElements =
                  {
                        new float[] {1,  0,  0,  0, 0},
                        new float[] {0,  1,  0,  0, 0},
                        new float[] {0,  0,  1,  0, 0},
                        new float[] {0,  0,  0,  1, 0},
                        new float[] {sb, sb, sb, 1, 1}
                                          };

            ColorMatrix cm = new ColorMatrix(colorMatrixElements);
            ImageAttributes imgattr = new ImageAttributes();
            Rectangle rc = new Rectangle(0, 0, img.Width, img.Height);
            Graphics g = Graphics.FromImage(img);
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            imgattr.SetColorMatrix(cm);
            g.DrawImage(img, rc, 0, 0, img.Width, img.Height, GraphicsUnit.Pixel, imgattr);

            //Clean everything up
            imgattr.Dispose();
            g.Dispose();

        }
	*/
}

util = new Utility();
